package org.easier.framework.beancopier.baidu;


import com.baidu.unbiz.easymapper.CopyByRefMapper;
import com.baidu.unbiz.easymapper.MapperFactory;
import com.baidu.unbiz.easymapper.exception.MappingException;
import com.baidu.unbiz.easymapper.metadata.ClassMap;
import com.baidu.unbiz.easymapper.metadata.MapperKey;
import com.baidu.unbiz.easymapper.metadata.TypeFactory;
import com.baidu.unbiz.easymapper.util.Memoizer;
import org.easier.framework.beancopier.CopierAdapter;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDateTime;
import java.util.*;


public class BaiduEasyCopier implements CopierAdapter {
    private final com.baidu.unbiz.easymapper.Mapper easymapper;
    private Memoizer<MapperKey, ClassMap<Object, Object>> cache;

    public BaiduEasyCopier() {
        this.easymapper = MapperFactory.getCopyByRefMapper();

        try {
            Field field = CopyByRefMapper.class.getDeclaredField("classMapCache");
            field.setAccessible(true);
            cache = (Memoizer<MapperKey, ClassMap<Object, Object>>) field.get(easymapper);
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
            ex.printStackTrace();
        }
    }

    public static Field[] joinFields(Field[] field1, Field[] field2) {
        Field[] fields = new Field[field1.length + field2.length];
        System.arraycopy(field1, 0, fields, 0, field1.length);
        System.arraycopy(field2, 0, fields, field1.length, field2.length);
        return fields;
    }

    @Override
    public <T> T map(Object source, Class<T> targetClass) {
        Class<?> oClazz = source.getClass();
        if (!isRegister(oClazz, targetClass)) {
            mapperRegister(oClazz, targetClass);
        }

        try {
            return easymapper.map(source, targetClass);
        } catch (MappingException ex) {
            mapperRegister(oClazz, targetClass);
            return easymapper.map(source, targetClass);
        }
    }

    @Override
    public <T> List<T> mapList(Object sources, Class<T> targetClass) {
        List<T> list = new ArrayList<>();
        if (sources instanceof List) {
            for (Object source : (List) sources) {
                list.add(map(source, targetClass));
            }
        }
        return list;
    }

    @Override
    public <T> T map(Object object, T target) {
        Class<?> sourceObjectClass = object.getClass();
        Class<?> destinationObjectClass = target.getClass();
        if (!isRegister(sourceObjectClass, destinationObjectClass)) {
            mapperRegister(sourceObjectClass, destinationObjectClass);
        }

        try {
            return easymapper.map(object, target);
        } catch (MappingException ex) {
            mapperRegister(sourceObjectClass, destinationObjectClass);
            return easymapper.map(object, target);
        }
    }

    public <T> void register(Class sourceClass, Class<T> targetClass) {
        easymapper.mapClass(sourceClass, targetClass).register();
    }

    public <T> void register(Class sourceClass, Class<T> targetClass, String... excludes) {
        easymapper.mapClass(sourceClass, targetClass).exclude(excludes).register();
    }

    public <T> boolean isRegister(Class sourceClass, Class<T> targetClass) {
        MapperKey key = new MapperKey(TypeFactory.valueOf(sourceClass), TypeFactory.valueOf(targetClass));
        return cache != null && cache.get(key) != null;
    }

    private void mapperRegister(Class<?> sourceClazz, Class<?> targetClazz) {
        if (Collection.class.isAssignableFrom(sourceClazz) || Map.class.isAssignableFrom(sourceClazz) || Iterator.class.isAssignableFrom(sourceClazz)) {
            return;
        }

        Field[] fields = getClassFields(sourceClazz);
        Field[] targetFields = getClassFields(targetClazz);

        List<String> excludes = new ArrayList<>();
        Map<String, Field> targetNames = fieldsToMap(targetFields);
        for (Field field : fields) {
            Class<?> clazz = field.getType();
            String fieldName = field.getName();
            if (!targetNames.containsKey(fieldName)) {
                continue;
            }

            Class<?> tClazz = targetNames.get(fieldName).getType();
            if (Enum.class.isAssignableFrom(tClazz) || Enum.class.isAssignableFrom(clazz)) {
                excludes.add(fieldName);
                continue;
            }

            if (!Objects.equals(clazz, tClazz)) {
                excludes.add(fieldName);
                continue;
            }

            if (isBaseDataType(clazz)) {
                continue;
            }

            Class<?> fieldType = field.getType();
            if ((Collection.class.isAssignableFrom(fieldType) || Map.class.isAssignableFrom(fieldType) || Iterator.class.isAssignableFrom(fieldType))
                    && !isEqualType(field, targetNames.get(fieldName))) {
                excludes.add(fieldName);
            }

            if (Objects.equals(tClazz, clazz)) {
                continue;
            }
            register(clazz, tClazz);
//            attributeRegister(clazz, tClazz);
            mapperRegister(clazz, tClazz);
        }
        register(sourceClazz, targetClazz, excludes.toArray(new String[excludes.size()]));
    }

    private HashMap<String, Field> fieldsToMap(Field[] targetFields) {
        HashMap<String, Field> map = new HashMap<>();
        for (Field field : targetFields) {
            map.put(field.getName(), field);
        }
        return map;
    }

    private Field[] getClassFields(Class clazz) {
        if (clazz == null || clazz.equals(Object.class)) {
            return null;
        }
        Field[] declaredFields = clazz.getDeclaredFields();
        Field[] afterFields = getClassFields(clazz.getSuperclass());
        if (afterFields == null) {
            return declaredFields;
        }

        return joinFields(afterFields, declaredFields);
    }

    private boolean isEqualType(Field fieldInput, Field fieldOutput) {
        return Objects.equals(fieldInput.getGenericType(), fieldOutput.getGenericType());
    }

    public static boolean isBaseDataType(Class clazz) {
        return clazz.isPrimitive() ||
                clazz.equals(String.class) ||
                clazz.equals(Date.class) ||
                clazz.equals(LocalDateTime.class) ||
                clazz.equals(Boolean.class) ||
                clazz.equals(Integer.class) ||
                clazz.equals(Long.class) ||
                clazz.equals(Double.class) ||
                clazz.equals(BigDecimal.class) ||
                clazz.equals(Float.class) ||
                clazz.equals(Short.class) ||
                clazz.equals(BigInteger.class) ||
                clazz.equals(Character.class) ||
                clazz.equals(Byte.class);
    }
}

